#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import asyncio
import logging
import re
import sys
from asyncio.subprocess import PIPE, Process, create_subprocess_exec
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager, contextmanager
from typing import IO

ENCODING = 'utf-8'

logging.basicConfig(level=logging.ERROR)

log = logging.getLogger('GDB')


@contextmanager
def hold_event(e: asyncio.Event):
    try:
        e.clear()
        yield e
    finally:
        e.set()


class GDB:
    SEPARATOR = b'(gdb) '

    class ExitError(BaseException):
        pass

    @classmethod
    @asynccontextmanager
    async def create(cls, args: list[str]) -> AsyncGenerator['GDB']:
        proc = await create_subprocess_exec(
            'gdb',
            '--args',
            *args,
            stdin=PIPE,
            stdout=PIPE,
            stderr=PIPE,
        )
        try:
            g = GDB(proc)
            log.info(await g._wait_ctrl())
            await g._exec('set pagination 0')
            yield g
            proc.terminate()
        finally:
            proc.kill()
            await proc.wait()

    def __init__(self, proc: Process):
        self._proc = proc
        self._running = asyncio.Event()
        self._event = asyncio.Event()
        self._event.set()

    async def wait_stop(self):
        return await self._running.wait()

    async def _wait_ctrl(self):
        STOP = re.compile(rb'\[Inferior \d+ \(process \d+\) exited normally\]')
        stdout = self._proc.stdout
        res = await stdout.readuntil(self.SEPARATOR)
        # res = await stdout.read(-1)
        lines = res.splitlines(keepends=True)
        assert lines[-1] == self.SEPARATOR
        if any(STOP.match(line) for line in lines):
            self._running.clear()
        return [line.decode(encoding=ENCODING) for line in lines[:-1]]

    async def _send(self, cmd: str):
        stdin = self._proc.stdin
        stdin.write((cmd + '\n').encode(encoding=ENCODING))
        await stdin.drain()

    async def _interupt(self):
        co = self._wait_ctrl()
        # self._proc.send_signal(signal.SIGINT)
        await self._send('interrupt')
        await self._send('')
        return await co

    async def _exec(self, cmd: str):
        log.info(cmd)
        # with hold_event(self._event):
        co = self._wait_ctrl()
        await self._send(cmd)
        res = await co
        log.info(res)
        return res

    async def run(self):
        assert not self._running.is_set()
        self._running.set()
        await self._exec('run &')
        # await self.wait_stop()
        # raise GDB.ExitError()

    async def bt_loop(self, delay: float, file: IO[str]):
        while self._running.is_set():
            await asyncio.sleep(delay)
            await self._interupt()
            bt = await self._exec('thread apply all bt')
            file.writelines(bt)
            file.flush()
            await self._exec('continue &')

    async def perf(self, file: IO[str], delay: float):
        await self.run()
        await asyncio.sleep(0.01)
        await self.bt_loop(delay, file)


async def main(args: list[str]):
    async with GDB.create(args) as g:
        await g.perf(file=sys.stdout, delay=0.01)


if __name__ == '__main__':
    asyncio.run(main(sys.argv[1:]))
